package ru.usu.gv.utils.mds;

import ru.usu.gv.utils.RandomUtilities;

import javax.vecmath.Point3d;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;

/**
 * @author spupyrev
 * 15.11.2008
 */
public class MDSUtilities
{
	private static final boolean ALLOWED_3D = false;

	public Point3d[] layout(double[][] dissimilarities, Point3d lb, Point3d rt)
	{
		try
		{
			Point3d[] res = performMDSSafe(dissimilarities);
			return fit(res, lb, rt);
		}
		catch (Exception e)
		{
			throw new RuntimeException(e);
		}
	}

	public Point3d[] fit(Point3d[] points, Point3d lb, Point3d rt)
	{
		Point3d[] res = points;

		double mx, Mx, my, My, mz, Mz;
		mx = Mx = res[0].x;
		my = My = res[0].y;
		mz = Mz = res[0].z;
		for (int i = 1; i < res.length; i++)
		{
			mx = Math.min(mx, res[i].x);
			Mx = Math.max(Mx, res[i].x);
			my = Math.min(my, res[i].y);
			My = Math.max(My, res[i].y);
			mz = Math.min(mz, res[i].z);
			Mz = Math.max(Mz, res[i].z);
		}

		double lenX = rt.x - lb.x;
		double lenY = rt.y - lb.y;
		double lenZ = rt.z - lb.z;

		double scaleX = 1, scaleY = 1, scaleZ = 1;
		if (Mx - mx > 1e-6)
			scaleX = (scaleX / (Mx - mx)) * lenX;
		if (My - my > 1e-6)
			scaleY = (scaleY / (My - my)) * lenY;
		if (Mz - mz > 1e-6)
			scaleZ = (scaleZ / (Mz - mz)) * lenZ;

		double cenx = (mx + Mx) / 2.0;
		double ceny = (my + My) / 2.0;
		double cenz = (mz + Mz) / 2.0;
		double cenX = (lb.x + rt.x) / 2.0;
		double cenY = (lb.y + rt.y) / 2.0;
		double cenZ = (lb.z + rt.z) / 2.0;

		for (int i = 0; i < res.length; i++)
		{
			Point3d p = res[i];
			p.sub(new Point3d(cenx, ceny, cenz));
			p.x *= scaleX;
			p.y *= scaleY;
			p.z *= scaleZ;
			p.add(new Point3d(cenX, cenY, cenZ));
		}

		return res;
	}

	private Point3d[] performMDS(double[][] dissim) throws IOException, InterruptedException, UnsupportedEncodingException
	{
		if (dissim.length == 1)
			return new Point3d[] { new Point3d(0, 0, 0) };

		if (dissim.length == 2)
			return new Point3d[] { new Point3d(dissim[0][0], dissim[0][1], 0), new Point3d(dissim[1][0], dissim[1][1], 0) };

		Process p = Runtime.getRuntime().exec("utils/hitmds2.exe");

		//System.out.println("Executing mds with size=" + dissim.length);
		//parameters
		BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(p.getOutputStream()));
		writer.write("#" + dissim.length + " " + dissim[0].length + "\n");
		//System.out.print("#" + dissim.length + " " + dissim[0].length + "\n");
		for (int i = 0; i < dissim.length; i++)
			for (int j = 0; j < dissim[i].length; j++)
			{
				writer.write("" + dissim[i][j]);
				//System.out.print("" + dissim[i][j]);
				if (j == dissim[i].length - 1)
				{
					writer.write("\n");
					//System.out.print("\n");
				}
				else
				{
					writer.write(" ");
					//System.out.print(" ");
				}
			}
		if (ALLOWED_3D)
			writer.write("#-3\n");
		else
			writer.write("#-2\n");
		writer.flush();
		writer.close();

		int returnValue = p.waitFor();
		if (returnValue != 0)
		{
			BufferedReader brCleanUp = new BufferedReader(new InputStreamReader(p.getErrorStream(), "CP866"));
			String line;
			while ((line = brCleanUp.readLine()) != null)
			{
				System.out.println("[Stderr] " + line);
			}
			brCleanUp.close();
			throw new RuntimeException("Non-zero exit code");
		}

		//result
		Point3d[] res = new Point3d[dissim.length];
		BufferedReader answerReader = new BufferedReader(new InputStreamReader(p.getInputStream()));
		String line;
		int i = 0;
		while ((line = answerReader.readLine()) != null)
		{
			double x = Double.valueOf(line.split(" ")[0]);
			double y = Double.valueOf(line.split(" ")[1]);
			double z = (ALLOWED_3D ? Double.valueOf(line.split(" ")[2]) : 0);
			res[i++] = new Point3d(x, y, z);
		}
		answerReader.close();
		return res;
	}

	private Point3d[] performMDSInternal(double[][] dissim)
	{
		if (dissim.length == 1)
			return new Point3d[] { new Point3d(0, 0, 0) };

		if (dissim.length == 2)
			return new Point3d[] { new Point3d(dissim[0][0], dissim[0][1], 0), new Point3d(dissim[1][0], dissim[1][1], 0) };

		//System.out.println("Executing mds with size=" + dissim.length);

		//result
		Point3d[] res = new Point3d[dissim.length];
		double[][] a = new HitMDS().evaluate(dissim, (ALLOWED_3D ? -3 : -2));
		for (int i = 0; i < a.length; i++)
		{
			for (int j = 0; j < a[i].length; j++)
				if (Double.isNaN(a[i][j]) || Double.isInfinite(a[i][j]))
					throw new NumberFormatException();

			double x = a[i][0];
			double y = a[i][1];
			double z = (ALLOWED_3D ? a[i][2] : 0);
			res[i] = new Point3d(x, y, z);
		}
		return res;
	}

	private Point3d[] performMDSSafe(double[][] dissim) throws IOException, InterruptedException, UnsupportedEncodingException
	{
		double EPS = 0.001;
		int ATTEMPT_COUNT = 5;
		for (int i = 0; i < ATTEMPT_COUNT; i++)
		{
			try
			{
				Point3d[] res = performMDSInternal(dissim);
				return res;
			}
			catch (NumberFormatException e)
			{
				System.err.println("NumberFormatException");

				int i1 = RandomUtilities.nextInt(dissim.length);
				int i2 = RandomUtilities.nextInt(dissim.length);
				while (i1 == i2)
					i2 = RandomUtilities.nextInt(dissim.length);

				dissim[i1][i2] += EPS;
				dissim[i2][i1] += EPS;
			}
		}

		return performMDS(dissim);
	}

}
